package com.gaojinqi.sys.core.shiro.filter;

import com.gaojinqi.base.common.constant.RespConst;
import com.gaojinqi.base.common.exception.AuthException;
import com.gaojinqi.base.common.exception.AuthExpiredException;
import com.gaojinqi.base.common.util.JsonUtil;
import com.gaojinqi.base.common.util.RequestResponseUtil;
import com.gaojinqi.base.common.vo.ResultVo;
import com.gaojinqi.base.core.util.IpUtil;
import com.gaojinqi.sys.core.auth.util.LoginContextHolder;
import com.gaojinqi.sys.core.shiro.token.JwtToken;
import com.gaojinqi.sys.core.shiro.util.JwtTokenHelper;
import com.gaojinqi.sys.core.shiro.util.UserCacheHelper;
import com.gaojinqi.sys.modular.rbac.model.LoginBo;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
import java.util.stream.Stream;

/**
 * Jwt 过滤器<Br/>
 * 支持restful url 的过滤链  JWT json web token 过滤器
 *
 * @author gaojinqi
 * @version 1.0
 * @since 2020年04月22日
 */
@Slf4j
public class JwtFilter extends AbstractRestPathMatchingFilter {

    public static final String FILTER_NAME = "jwt";

    private UserCacheHelper userCacheHelper;

    private JwtTokenHelper jwtTokenHelper;

    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception {
        Subject subject = this.getSubject(servletRequest, servletResponse);

        boolean isJwtRequest = (null != subject && !subject.isAuthenticated()) && isJwtRequest(servletRequest);
        HttpServletRequest httpServletRequest = WebUtils.toHttp(servletRequest);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(servletResponse);
        if (isJwtRequest) {
            AuthenticationToken token = createJwtToken(servletRequest);
            try {
                subject.login(token);
                String userName = (String) token.getPrincipal();
                // 放入 threadLocal
                pushInContext(userName);
                return this.checkRoles(httpServletResponse, subject, mappedValue);
            } catch (AuthenticationException | AuthException e) {

                // 如果是JWT过期
                if (e instanceof AuthExpiredException || e instanceof ExpiredCredentialsException) {
                    // 生成jwt时，设置过期时间，以及将jwt放入缓存中，在缓存设置过期时间+刷新时间
                    // 当jwt过期时抛出异常，缓存存在时则代表还有刷新时间，这时重新生成新的Jwt返回给客户端，并延长时间
                    // 当存储在缓存的JWT没有过期，即refresh time 没有过期，可以一直刷新
                    String userName = (String) token.getPrincipal();
                    String jwt = (String) token.getCredentials();
                    String jwtCache = jwtTokenHelper.getTokenCache(userName);
                    if (Objects.equals(jwtCache, jwt) && StringUtils.hasText(userName)) {
                        // 重新申请新的JWT
                        String newJwt = jwtTokenHelper.createAndCacheToken(userName);
                        // 验证权限
                        try {
                            token = new JwtToken(newJwt, servletRequest.getRemoteAddr(), jwtTokenHelper);
                            subject.login(token);
                            // 放入 threadLocal
                            pushInContext(userName);
                        } catch (AuthenticationException ae) {
                            // 出错则移除会话缓存
                            jwtTokenHelper.removeTokenCache(userName);
                            log.error(IpUtil.getIpAddr(httpServletRequest) + " --- JWT刷新令牌失败 msg = " + ae.getMessage(), ae);
                            this.fail(httpServletResponse, RespConst.RespEnum.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), null);
                            return false;
                        }
                        return this.checkRoles(httpServletResponse, subject, mappedValue);
                    } else {
                        // jwt时间失效过期,jwt refresh time失效 返回jwt过期客户端重新登录
                        this.fail(httpServletResponse, RespConst.RespEnum.EXPIRED, HttpStatus.UNAUTHORIZED.value(), null);
                        return false;
                    }

                }
                // 其他的判断为JWT错误无效
                this.fail(httpServletResponse, RespConst.RespEnum.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), null);
                return false;

            } catch (Exception e) {
                // 其他错误
                log.error(IpUtil.getIpAddr(httpServletRequest) + " --- JWT认证失败 msg = " + e.getMessage(), e);
                this.fail(httpServletResponse, RespConst.RespEnum.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), null);
                return false;
            }
        } else {
            // 请求未携带jwt 判断为无效请求
            this.fail(httpServletResponse, RespConst.RespEnum.BAD_REQUEST, HttpStatus.UNAUTHORIZED.value(), "请重新登录");
            return false;
        }
    }

    // 放入 threadLocal
    private void pushInContext(String userName) {
        LoginBo loginBo = userCacheHelper.getByName(userName, LoginBo.class);
        LoginContextHolder.getContext().set(loginBo);
    }

    private boolean checkRoles(HttpServletResponse httpServletResponse, Subject subject, Object mappedValue) {
        String[] rolesArray = (String[]) mappedValue;
        boolean hasRoles = rolesArray == null || rolesArray.length == 0 || Stream.of(rolesArray).anyMatch(role -> subject.hasRole(role.trim()));
        if (!hasRoles) {
            this.fail(httpServletResponse, RespConst.RespEnum.NO_PERMISSIONS, HttpStatus.FORBIDDEN.value(), null);
        }
        return hasRoles;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        return false;
    }


    /**
     * 是否是jwt请求
     */
    private boolean isJwtRequest(ServletRequest request) {
        String jwt = jwtTokenHelper.getJwt(request);
        return (request instanceof HttpServletRequest)
                && !StringUtils.isEmpty(jwt);
    }

    private AuthenticationToken createJwtToken(ServletRequest request) {
        String jwt = jwtTokenHelper.getJwt(request);
        String ipHost = request.getRemoteAddr();
        return new JwtToken(jwt, ipHost, jwtTokenHelper);
    }

    public void setUserCacheHelper(UserCacheHelper userCacheHelper) {
        this.userCacheHelper = userCacheHelper;
    }

    public void setJwtTokenHelper(JwtTokenHelper jwtTokenHelper) {
        this.jwtTokenHelper = jwtTokenHelper;
    }

    private void fail(HttpServletResponse httpServletResponse, RespConst.RespEnum respEnum, Integer httpStatue, String msg) {
        ResultVo<Object> fail = ResultVo.fail(respEnum);
        if (msg != null) {
            fail.setMessage(msg);
        }
        if (httpStatue != null) {
            httpServletResponse.setStatus(httpStatue);
        }
        RequestResponseUtil.responseWrite(JsonUtil.toJSONString(fail), httpServletResponse);
    }

}
